/**
 * Copyright (C) 2013 DaiKit.com - daikit4gxt module (admin@daikit.com)
 *
 *         Project home : http://code.daikit.com/daikit4gxt
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.daikit.daikit4gxt.client.editor;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.daikit.commons.shared.bean.AbstractDkModifiableBeanWithId;
import com.daikit.commons.shared.utils.DkObjectUtils;
import com.google.gwt.editor.client.EditorContext;
import com.google.gwt.editor.client.EditorVisitor;
import com.google.gwt.editor.client.HasEditorDelegate;
import com.google.gwt.editor.client.LeafValueEditor;
import com.sencha.gxt.data.client.editor.ListStoreEditor;
import com.sencha.gxt.data.shared.ListStore;


/**
 * {@link EditorVisitor} permitting to update {@link AbstractDkEditedBeanAwareEditor} dirty status and
 * {@link AbstractDkModifiableBeanWithId} dirty status, dirty paths and deletedChildrenIds
 * 
 * @author tcaselli
 * @author rdupuis
 * @version $Revision$ Last modifier: $Author$ Last commit: $Date$
 */
@SuppressWarnings("rawtypes")
public class DkPreFlushVisitor extends EditorVisitor
{
	private boolean updateModel = false;

	/**
	 * Constructor
	 * 
	 * @param updateModelDirtyStatus
	 *           false by default
	 */
	public DkPreFlushVisitor(final boolean updateModelDirtyStatus)
	{
		updateModel = updateModelDirtyStatus;
	}

	private final List<AbstractDkEditedBeanAwareEditor> stack = new ArrayList<AbstractDkEditedBeanAwareEditor>();

	@Override
	public <T extends Object> boolean visit(final com.google.gwt.editor.client.EditorContext<T> ctx)
	{
		if (ctx.getEditor() instanceof AbstractDkEditedBeanAwareEditor)
		{
			stack.add((AbstractDkEditedBeanAwareEditor<T>) ctx.getEditor());
		}
		return true;
	};

	@Override
	public <T> void endVisit(final EditorContext<T> ctx)
	{
		if (customVisit(ctx))
		{
			visitLeaf(ctx);
			visitListRemovableEditor(ctx);
			visitListStoreEditor(ctx);
			visitTreeStoreEditor(ctx);
		}
		if (ctx.getEditor() instanceof AbstractDkEditedBeanAwareEditor)
		{
			if (!stack.isEmpty())
			{
				stack.remove(stack.size() - 1);
			}
		}
	}

	/**
	 * To be overridden to provide custom visitor
	 * 
	 * @param ctx
	 *           the {@link EditorContext}
	 * @return whether to apply other visitors or not. (Return true by default, and should return false if your custom
	 *         implementation was effectively taking into account the given context)
	 */
	protected <T> boolean customVisit(final EditorContext<T> ctx)
	{
		return true;
	}

	/**
	 * Compare model value to editor value. if they are not the same all ancestor editor will be flagged as dirty.
	 * Moreover the direct AbstractModifiableBeanWithId parent to the leaf editor will be updated. After being updated
	 * the parent will contain the leafEditor attribute name.
	 * 
	 * @param ctx
	 */
	private <T> void visitLeaf(final EditorContext<T> ctx)
	{
		final LeafValueEditor<T> editor = ctx.asLeafValueEditor();
		if (editor != null)
		{
			final T modelValue = ctx.getFromModel();
			final T newValue = editor.getValue();

			if (!DkObjectUtils.equalsWithNull(modelValue, newValue) || newValue instanceof AbstractDkModifiableBeanWithId
					&& ((AbstractDkModifiableBeanWithId) newValue).isDirty())
			{
				final AbstractDkEditedBeanAwareEditor<T> parent = getParent();
				if (parent != null)
				{
					// iterate through parents starting from the oldest
					updateIterativelyDirtyStatusAndPath(ctx.getEditorDelegate().getPath());
				}
			}
		}
	}

	/**
	 * If a ListStoreRemovableEditor is visited this method will update ancestor editor with dirty flag. It will also
	 * populate the direct parent editor with the current editor attributeName and the list of removed items id.
	 * 
	 * @param ctx
	 */
	@SuppressWarnings("unchecked")
	private <T> void visitListRemovableEditor(final EditorContext<T> ctx)
	{
		final HasEditorDelegate<T> listEditor = ctx.asHasEditorDelegate();
		if (listEditor instanceof DkListStoreRemovableEditor)
		{
			final List<T> removedItems = ((DkListStoreRemovableEditor<T>) listEditor).getRemovedItems();
			if (removedItems != null && !removedItems.isEmpty())
			{
				// iterate through parent editors starting from the oldest
				updateIterativelyDirtyStatusAndPath(ctx.getEditorDelegate().getPath());
				// updated removed ids
				final AbstractDkEditedBeanAwareEditor<T> parent = getParent();
				if (parent != null && updateModel && parent.getEditedModel() instanceof AbstractDkModifiableBeanWithId)
				{
					final Set<String> removedIds = new HashSet<String>();
					for (final T item : removedItems)
					{
						if (item instanceof AbstractDkModifiableBeanWithId)
						{
							removedIds.add(((AbstractDkModifiableBeanWithId) item).getId());
						}
					}
					((AbstractDkModifiableBeanWithId) parent.getEditedModel()).getDeletedChildrenIds().put(
							getLastAttributePath(ctx.getEditorDelegate().getPath()), removedIds);
				}
			}
		}
	}

	/**
	 * Visit child {@link ListStoreEditor}
	 * 
	 * @param ctx
	 */
	@SuppressWarnings("unchecked")
	private <T> void visitListStoreEditor(final EditorContext<T> ctx)
	{
		final HasEditorDelegate<T> editor = ctx.asHasEditorDelegate();
		if (editor instanceof ListStoreEditor || editor instanceof DkListStoreRemovableEditor)
		{
			boolean isDirty = false;
			if (editor instanceof DkListStoreRemovableEditor)
			{
				final DkListStoreRemovableEditor<T> removableEditor = (DkListStoreRemovableEditor<T>) editor;
				if (removableEditor.getOriginalModels().size() != removableEditor.getStore().getAll().size())
				{
					isDirty = true;
				}
				else
				{
					final List<T> storeList = removableEditor.getStore().getAll();
					for (final T model : removableEditor.getOriginalModels())
					{
						if (!storeList.contains(model))
						{
							isDirty = true;
							break;
						}
					}
				}
			}
			if (!isDirty)
			{
				final ListStore<T> listStore = editor instanceof ListStoreEditor ? ((ListStoreEditor<T>) editor).getStore()
						: ((DkListStoreRemovableEditor<T>) editor).getStore();

				for (final T listElement : listStore.getAll())
				{
					if (listElement instanceof AbstractDkModifiableBeanWithId
							&& ((AbstractDkModifiableBeanWithId) listElement).isDirty())
					{
						isDirty = true;
						break;
					}
				}
			}
			if (isDirty)
			{
				updateIterativelyDirtyStatusAndPath(ctx.getEditorDelegate().getPath());
			}
		}
	}

	/**
	 * Visit child {@link DkTreeStoreEditor}
	 * 
	 * @param ctx
	 */
	private <T> void visitTreeStoreEditor(final EditorContext<T> ctx)
	{
		final HasEditorDelegate<T> editor = ctx.asHasEditorDelegate();
		if (editor instanceof DkTreeStoreEditor)
		{
			final DkTreeStoreEditor treeStoreEditor = (DkTreeStoreEditor) editor;
			boolean isDirty = false;
			for (final Object listElement : treeStoreEditor.getStore().getAll())
			{
				if (listElement instanceof AbstractDkModifiableBeanWithId && ((AbstractDkModifiableBeanWithId) listElement).isDirty())
				{
					isDirty = true;
					break;
				}
			}
			if (isDirty)
			{
				updateIterativelyDirtyStatusAndPath(ctx.getEditorDelegate().getPath());
			}
		}
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// UTILITY METHODS
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * 
	 * @param contextDelegatePath
	 *           the current context delegate path
	 */
	protected void updateIterativelyDirtyStatusAndPath(final String contextDelegatePath)
	{
		// iterate through parent editors starting from the oldest
		for (int i = 0; i < stack.size(); i++)
		{
			final AbstractDkEditedBeanAwareEditor ancestor = stack.get(i);
			ancestor.setDirty(true);
			if (updateModel && ancestor.getEditedModel() instanceof AbstractDkModifiableBeanWithId)
			{
				final AbstractDkModifiableBeanWithId data = (AbstractDkModifiableBeanWithId) ancestor.getEditedModel();
				data.setDirty(true);
				if (i == stack.size() - 1)
				{
					// update status in bean
					data.getDirtyPaths().add(getLastAttributePath(contextDelegatePath));
				}
				else
				{
					final AbstractDkEditedBeanAwareEditor childAncestor = stack.get(i + 1);
					data.getDirtyPaths().add(getLastAttributePath(childAncestor.getDelegate().getPath()));
				}
			}
		}
	}

	private String getLastAttributePath(final String absolutePath)
	{
		return absolutePath.substring(absolutePath.lastIndexOf(".") + 1);
	}


	@SuppressWarnings("unchecked")
	private <T> AbstractDkEditedBeanAwareEditor<T> getParent()
	{
		return stack.isEmpty() ? null : stack.get(stack.size() - 1);
	}
}
